package org.eclipse.jdt.internal.compiler.ast;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.impl.ReferenceContext;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemMethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReasons;
import org.eclipse.jdt.internal.compiler.lookup.RawTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TagBits;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities;

/**
 * Operator overload implementation prototype.
 * <ul>process 3 kind of case:
 * <li> default operator names, such as add, subtract
 * <li> annotated method,
 * <pre>
 * <code>
 * 	package javax.syntax.sugar;
 * 	public static @interface Operator{
			string value();
			//or OperatorEnum type
		}
		class Model{
			@Operator(PLUS)
			public void add(){}
		}
		new added class maybe copy this annotation, 
		Don't compare with Annotation.class, just for name "javax.syntax.sugar.Operator", 
		because of OSGi bundle dependency;
</code>
 * </pre> 
 * <li> external described annotated method, ON document format
 * Operator{ 
 * 	Java.math.BigInteger{PLUS: add; minus: anotherSubtract}
 *		test.model.Account {PLUS: withdraw; minus: rob}
 * }
 * configure JDT preference to setting external document location.
 * </ul>
 * @author XIANGYA
 * @since 0.1
 * @version 0.1
 */
public class Expression2MessageSend extends MessageSend{
	public final static char[][] DefaultOperatorNames=new char[][]{//
		"add".toCharArray(), //$NON-NLS-1$
		"subtract".toCharArray(),//$NON-NLS-1$
		"multiply".toCharArray(),//$NON-NLS-1$
		"divide".toCharArray(),//$NON-NLS-1$
		};
	
	private TypeBinding[] resolvedArgumentTypes;
	public Expression2MessageSend(BinaryExpression binaryExpression, TypeBinding leftType, TypeBinding rightType) {
		this.receiver = binaryExpression.left;
		switch((binaryExpression.bits & ASTNode.OperatorMASK) >> ASTNode.OperatorSHIFT){
			case OperatorIds.PLUS:
				this.selector = DefaultOperatorNames[0];
				break;
			case OperatorIds.MINUS:
				this.selector = DefaultOperatorNames[1];
				break;
			case OperatorIds.MULTIPLY:
				this.selector = DefaultOperatorNames[2];
				break;
			case OperatorIds.DIVIDE:
				this.selector = DefaultOperatorNames[3];
				break;
		}
		this.arguments = new Expression[] { binaryExpression.right };
		this.sourceStart = binaryExpression.sourceStart;
		this.sourceEnd = binaryExpression.sourceEnd;
		this.actualReceiverType = leftType;
		this.resolvedArgumentTypes = new TypeBinding[]{rightType};
	}
	
	public TypeBinding resolveType(BlockScope scope) {

		// Answer the signature return type
		// Base type promotion

		this.constant = Constant.NotAConstant;
		boolean receiverCast = false, argsContainCast = false;
		if (this.receiver instanceof CastExpression) {
			this.receiver.bits |= ASTNode.DisableUnnecessaryCastCheck; // will check later on
			receiverCast = true;
		}
		//this.actualReceiverType = this.receiver.resolveType(scope);
		boolean receiverIsType = this.receiver instanceof NameReference && (((NameReference) this.receiver).bits & Binding.TYPE) != 0;
		if (receiverCast && this.actualReceiverType != null) {
			 // due to change of declaring class with receiver type, only identity cast should be notified
			if (((CastExpression)this.receiver).expression.resolvedType == this.actualReceiverType) {
				return null;
				//scope.problemReporter().unnecessaryCast((CastExpression)this.receiver);
			}
		}
		// resolve type arguments (for generic constructor call)
		if (this.typeArguments != null) {
			int length = this.typeArguments.length;
			boolean argHasError = scope.compilerOptions().sourceLevel < ClassFileConstants.JDK1_5; // typeChecks all arguments
			this.genericTypeArguments = new TypeBinding[length];
			for (int i = 0; i < length; i++) {
				TypeReference typeReference = this.typeArguments[i];
				if ((this.genericTypeArguments[i] = typeReference.resolveType(scope, true /* check bounds*/)) == null) {
					argHasError = true;
				}
				if (argHasError && typeReference instanceof Wildcard) {
					return null;
					//scope.problemReporter().illegalUsageOfWildcard(typeReference);
				}
			}
			if (argHasError) {
				if (this.arguments != null) { // still attempt to resolve arguments
					for (int i = 0, max = this.arguments.length; i < max; i++) {
						this.arguments[i].resolveType(scope);
					}
				}
				return null;
			}
		}
		// will check for null after args are resolved
//		TypeBinding[] argumentTypes = Binding.NO_PARAMETERS;
		TypeBinding[] argumentTypes = this.resolvedArgumentTypes;
		if (this.arguments != null) {
//			boolean argHasError = false; // typeChecks all arguments
			int length = this.arguments.length;
			boolean argHasError = this.resolvedArgumentTypes[0]==null;
//			argumentTypes = new TypeBinding[length];
//			for (int i = 0; i < length; i++){
//				Expression argument = this.arguments[i];
//				if (argument instanceof CastExpression) {
//					argument.bits |= ASTNode.DisableUnnecessaryCastCheck; // will check later on
//					argsContainCast = true;
//				}
//				if ((argumentTypes[i] = argument.resolveType(scope)) == null){
//					argHasError = true;
//				}
//			}
			if (argHasError) {
				if (this.actualReceiverType instanceof ReferenceBinding) {
					//  record a best guess, for clients who need hint about possible method match
					TypeBinding[] pseudoArgs = new TypeBinding[length];
					for (int i = length; --i >= 0;)
						pseudoArgs[i] = argumentTypes[i] == null ? TypeBinding.NULL : argumentTypes[i]; // replace args with errors with null type
					this.binding =
						this.receiver.isImplicitThis()
							? scope.getImplicitMethod(this.selector, pseudoArgs, this)
							: scope.findMethod((ReferenceBinding) this.actualReceiverType, this.selector, pseudoArgs, this);
					if (this.binding != null && !this.binding.isValidBinding()) {
						MethodBinding closestMatch = ((ProblemMethodBinding)this.binding).closestMatch;
						// record the closest match, for clients who may still need hint about possible method match
						if (closestMatch != null) {
							if (closestMatch.original().typeVariables != Binding.NO_TYPE_VARIABLES) { // generic method
								// shouldn't return generic method outside its context, rather convert it to raw method (175409)
								closestMatch = scope.environment().createParameterizedGenericMethod(closestMatch.original(), (RawTypeBinding)null);
							}
							this.binding = closestMatch;
							MethodBinding closestMatchOriginal = closestMatch.original();
							if (closestMatchOriginal.isOrEnclosedByPrivateType() && !scope.isDefinedInMethod(closestMatchOriginal)) {
								// ignore cases where method is used from within inside itself (e.g. direct recursions)
								closestMatchOriginal.modifiers |= ExtraCompilerModifiers.AccLocallyUsed;
							}
						}
					}
				}
				return null;
			}
		}
		if (this.actualReceiverType == null) {
			return null;
		}
		// base type cannot receive any message
		if (this.actualReceiverType.isBaseType()) {
			return null;
			//	scope.problemReporter().errorNoMethodFor(this, this.actualReceiverType, argumentTypes);
		}
		this.binding = this.receiver.isImplicitThis()
				? scope.getImplicitMethod(this.selector, argumentTypes, this)
				: scope.getMethod(this.actualReceiverType, this.selector, argumentTypes, this);
		if (!this.binding.isValidBinding()) {
			if (this.binding.declaringClass == null) {
				if (this.actualReceiverType instanceof ReferenceBinding) {
					this.binding.declaringClass = (ReferenceBinding) this.actualReceiverType;
				} else {
					return null;
//					scope.problemReporter().errorNoMethodFor(this, this.actualReceiverType, argumentTypes);
				}
			}
			scope.problemReporter().invalidMethod(this, this.binding);
			MethodBinding closestMatch = ((ProblemMethodBinding)this.binding).closestMatch;
			switch (this.binding.problemId()) {
				case ProblemReasons.Ambiguous :
					break; // no resilience on ambiguous
				case ProblemReasons.NotVisible :
				case ProblemReasons.NonStaticReferenceInConstructorInvocation :
				case ProblemReasons.NonStaticReferenceInStaticContext :
				case ProblemReasons.ReceiverTypeNotVisible :
				case ProblemReasons.ParameterBoundMismatch :
					// only steal returnType in cases listed above
					if (closestMatch != null) this.resolvedType = closestMatch.returnType;
					break;
			}
			// record the closest match, for clients who may still need hint about possible method match
			if (closestMatch != null) {
				this.binding = closestMatch;
				MethodBinding closestMatchOriginal = closestMatch.original();
				if (closestMatchOriginal.isOrEnclosedByPrivateType() && !scope.isDefinedInMethod(closestMatchOriginal)) {
					// ignore cases where method is used from within inside itself (e.g. direct recursions)
					closestMatchOriginal.modifiers |= ExtraCompilerModifiers.AccLocallyUsed;
				}
			}
			return (this.resolvedType != null && (this.resolvedType.tagBits & TagBits.HasMissingType) == 0)
							? this.resolvedType
							: null;
		}
		if ((this.binding.tagBits & TagBits.HasMissingType) != 0) {
			scope.problemReporter().missingTypeInMethod(this, this.binding);
		}
		final CompilerOptions compilerOptions = scope.compilerOptions();
		if (!this.binding.isStatic()) {
			// the "receiver" must not be a type
			if (receiverIsType) {
				scope.problemReporter().mustUseAStaticMethod(this, this.binding);
				if (this.actualReceiverType.isRawType()
						&& (this.receiver.bits & ASTNode.IgnoreRawTypeCheck) == 0
						&& compilerOptions.getSeverity(CompilerOptions.RawTypeReference) != ProblemSeverities.Ignore) {
					scope.problemReporter().rawTypeReference(this.receiver, this.actualReceiverType);
				}
			} else {
				// handle indirect inheritance thru variable secondary bound
				// receiver may receive generic cast, as part of implicit conversion
				TypeBinding oldReceiverType = this.actualReceiverType;
				this.actualReceiverType = this.actualReceiverType.getErasureCompatibleType(this.binding.declaringClass);
				this.receiver.computeConversion(scope, this.actualReceiverType, this.actualReceiverType);
				if (this.actualReceiverType != oldReceiverType && this.receiver.postConversionType(scope) != this.actualReceiverType) { // record need for explicit cast at codegen since receiver could not handle it
					this.bits |= NeedReceiverGenericCast;
				}
			}
		} else {
			// static message invoked through receiver? legal but unoptimal (optional warning).
			if (!(this.receiver.isImplicitThis() || this.receiver.isSuper() || receiverIsType)) {
				scope.problemReporter().nonStaticAccessToStaticMethod(this, this.binding);
			}
			if (!this.receiver.isImplicitThis() && this.binding.declaringClass != this.actualReceiverType) {
				scope.problemReporter().indirectAccessToStaticMethod(this, this.binding);
			}
		}
		if (checkInvocationArguments(scope, this.receiver, this.actualReceiverType, this.binding, this.arguments, argumentTypes, argsContainCast, this)) {
			this.bits |= ASTNode.Unchecked;
		}

		//-------message send that are known to fail at compile time-----------
		if (this.binding.isAbstract()) {
			if (this.receiver.isSuper()) {
				scope.problemReporter().cannotDireclyInvokeAbstractMethod(this, this.binding);
			}
			// abstract private methods cannot occur nor abstract static............
		}
		if (isMethodUseDeprecated(this.binding, scope, true))
			scope.problemReporter().deprecatedMethod(this.binding, this);

		// from 1.5 source level on, array#clone() returns the array type (but binding still shows Object)
		if (this.binding == scope.environment().arrayClone && compilerOptions.sourceLevel >= ClassFileConstants.JDK1_5) {
			this.resolvedType = this.actualReceiverType;
		} else {
			TypeBinding returnType;
			if ((this.bits & ASTNode.Unchecked) != 0 && this.genericTypeArguments == null) {
				returnType = this.binding.original().returnType;
				if (returnType != null) {
					returnType = scope.environment().convertToRawType(returnType.erasure(), true);
				}
			} else {
				returnType = this.binding.returnType;
				if (returnType != null) {
					returnType = returnType.capture(scope, this.sourceEnd);
				}
			}
			this.resolvedType = returnType;
		}
		if (this.receiver.isSuper() && compilerOptions.getSeverity(CompilerOptions.OverridingMethodWithoutSuperInvocation) != ProblemSeverities.Ignore) {
			final ReferenceContext referenceContext = scope.methodScope().referenceContext;
			if (referenceContext instanceof AbstractMethodDeclaration) {
				final AbstractMethodDeclaration abstractMethodDeclaration = (AbstractMethodDeclaration) referenceContext;
				MethodBinding enclosingMethodBinding = abstractMethodDeclaration.binding;
				if (enclosingMethodBinding.isOverriding()
						&& CharOperation.equals(this.binding.selector, enclosingMethodBinding.selector)
						&& this.binding.areParametersEqual(enclosingMethodBinding)) {
					abstractMethodDeclaration.bits |= ASTNode.OverridingMethodWithSupercall;
				}
			}
		}
		if (this.typeArguments != null && this.binding.original().typeVariables == Binding.NO_TYPE_VARIABLES) {
			scope.problemReporter().unnecessaryTypeArgumentsForMethodInvocation(this.binding, this.genericTypeArguments, this.typeArguments);
		}
		return (this.resolvedType.tagBits & TagBits.HasMissingType) == 0
					? this.resolvedType
					: null;
	}
}
